package com.tiny.tinyweb.core.test;

import java.sql.Connection;

import javax.sql.DataSource;

import org.dbunit.Assertion;
import org.dbunit.database.DatabaseConfig;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.IDataSet;
import org.dbunit.ext.mysql.MySqlDataTypeFactory;
import org.dbunit.operation.DatabaseOperation;
import org.dbunit.util.fileloader.DataFileLoader;
import org.dbunit.util.fileloader.FlatXmlDataFileLoader;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
import org.springframework.util.Assert;

import com.tiny.tinyweb.core.annotation.DaoTestConfiguration;
import com.tiny.tinyweb.core.annotation.DaoTestData;
import com.tiny.tinyweb.core.annotation.DaoTestDataExpected;
import com.tiny.tinyweb.core.utils.DataSetUtils;

/**
 * 实现自
 * {@link org.springframework.test.context.transaction.TransactionalTestExecutionListener}
 * 的监听器类。该来用来处理dbunit的数据库配置和数据集管理。以测试用例为单位管理事务，测试数据会在测试执行后回滚。 测试的执行顺序如下:
 * <ul>
 * <li>{@link TransactionalTestExecutionListener#beforeTestClass}</li>
 * <li>{@link org.junit.BeforeClass}注解的方法</li>
 * <li>{@link TransactionalTestExecutionListener#prepareTestInstance}</li>
 * <li>{@link TransactionalTestExecutionListener#beforeTestMethod}</li>
 * <li>{@link org.junit.Before}注解的方法</li>
 * <li>{@link TransactionalTestExecutionListener#afterTestMethod}</li>
 * <li>{@link org.junit.After}注解的方法</li>
 * <li>{@link TransactionalTestExecutionListener#afterTestClass}</li>
 * <li>{@link org.junit.AfterClass}注解的方法</li>
 * </ul>
 *
 * @see {@link org.springframework.test.context.TransactionalTestExecutionListener}
 * @author wxiaobin
 */
public class UnitTestDaoExecutionListener extends TransactionalTestExecutionListener {
    /**
     * 用来加载数据集文件的加载器。
     */
    private DataFileLoader dataFileLoader;

    /**
     * 支持事务的数据源代理，用来包装从配置文件加载的数据源。
     */
    private TransactionAwareDataSourceProxy txDataSource;

    /**
     * 由{@link org.springframework.jdbc.datasource.DataSourceUtils}创建的数据库连接。
     */
    private Connection conn;

    /**
     * dbunit的数据库连接，从{@link conn}包装而来。
     */
    private IDatabaseConnection connDbunit;

    /**
     * 测试类的包路径，用来加载相同数据集文件。
     */
    private String packagePath;

    /**
     * 获得测试类的包路径，包装支持事务的数据源代理对象。
     *
     * @param testContext Spring测试框架管理的测试上下文
     * @see {@link org.springframework.test.context.TransactionalTestExecutionListener#beforeTestClass(TestContext)}
     */
    @Override
    public void beforeTestClass(TestContext testContext) throws Exception {
        // 为加载数据集文件获取测试类的包路径
        this.packagePath = "/" + testContext.getTestClass().getPackage().getName().replace(".", "/") + "/";

        // 包装支持事务的数据源代理对象
        String dataSource = "dataSource";
        DaoTestConfiguration conf = testContext.getTestClass().getAnnotation(DaoTestConfiguration.class);
        if (conf != null) {
        	dataSource = conf.dataSource();
        }
        this.txDataSource = new TransactionAwareDataSourceProxy(
                (DataSource) testContext.getApplicationContext().getBean(dataSource));
    }

    /**
     * 准备 dbunit 测试环境，首先获得数据库连接对象，并将其包装成 dbunit 的数据库连接对象，然后根据
     * {@link DaoTestConfiguration} 注解的设置值来配置 dbunit 数据库连接，最后加载数据集文件，准备测试数据。
     *
     * @param testContext Spring测试框架管理的测试上下文
     * @see {@link org.springframework.test.context.TransactionalTestExecutionListener#beforeTestMethod(TestContext)}
     */
    @Override
    public void beforeTestMethod(TestContext testContext) throws Exception {
        super.beforeTestMethod(testContext);
        // 取得{@link DaoTestConfiguration}注解
        DaoTestConfiguration conf = testContext.getTestClass().getAnnotation(DaoTestConfiguration.class);

        // 从数据源获得数据库连接，并将其包装成dbunit的数据库连接对象
        this.conn = DataSourceUtils.getConnection(this.txDataSource);
        if (conf != null && !"".equals(conf.schema())) {
        	this.connDbunit = new DatabaseConnection(this.conn, conf.schema());
        } else {
        	this.connDbunit = new DatabaseConnection(this.conn);
        }

        // 获得当前连接的数据库配置对象
        DatabaseConfig config = this.connDbunit.getConfig();
        // 处理数据库配置
        if (conf != null) {
            // 根据DaoTestConfiguration注解的配置创建数据集文件加载器
            this.dataFileLoader = conf.dataFileLoader().newInstance();
            // 根据DaoTestConfiguration注解的配置来设置忽略大小写标志
            config.setProperty(DatabaseConfig.FEATURE_CASE_SENSITIVE_TABLE_NAMES,
                    conf.ignoreCase());
            // 根据DaoTestConfiguration注解的配置创建数据类型工厂
            config.setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY,
                    conf.dataTypeFactory().newInstance());
        } else {
            // 创建默认的XML数据集文件加载器
            this.dataFileLoader = new FlatXmlDataFileLoader();
            // 设置忽略大小写
            config.setProperty(DatabaseConfig.FEATURE_CASE_SENSITIVE_TABLE_NAMES, true);
            // 创建默认的mySQL数据类型工厂
            config.setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY, new MySqlDataTypeFactory());
        }

        // 检查是否指定了数据集文件
        DaoTestData testData = testContext.getTestMethod().getAnnotation(DaoTestData.class);
        if (testData != null) {
            Assert.notEmpty(testData.value(), "the data set files of @DaoTestData must not be null");
            // 准备测试数据，使用“先清除再插入”的更新模式
            for (String filename : testData.value()) {
                DatabaseOperation.CLEAN_INSERT.execute(this.connDbunit, DataSetUtils.wrapDataSet(
                        this.dataFileLoader.load(this.getRealFileName(filename))));
            }
        }
    }

    /**
     * 测试执行后根据 {@link DaoTestDataExpected} 注解指定的数据集断言数据库的状态是否满足期待值。
     *
     * @param testContext Spring测试框架管理的测试上下文
     * @see {@link org.springframework.test.context.TransactionalTestExecutionListener#afterTestMethod(TestContext)}
     */
    @Override
    public void afterTestMethod(TestContext testContext) throws Exception {
        try {
            // 检查是否指定了需要断言的数据集文件
            DaoTestDataExpected testDataExpected = testContext.getTestMethod().getAnnotation(
                    DaoTestDataExpected.class);
            if (testDataExpected != null) {
                Assert.notEmpty(testDataExpected.value(),
                        "the expected data set files of @DaoTestDataExpected must not be null");
                // 加载当前数据库中的数据到数据集中
                IDataSet actual = this.connDbunit.createDataSet();
                // 加载由DaoTestDataExpected注解指定的数据集
                for (String filename : testDataExpected.value()) {
                    IDataSet expected = DataSetUtils.wrapDataSet(
                            this.dataFileLoader.load(this.getRealFileName(filename)));
                    // 断言数据库的状态是否和期待值一致
                    for (String tableName : expected.getTableNames()) {
                        Assertion.assertEquals(expected.getTable(tableName),
                                actual.getTable(tableName));
                    }
                }
            }
        } finally {
            super.afterTestMethod(testContext);
        }
    }

    /**
     * 获得数据集文件的全路径。
     *
     * @param fileName 数据集文件的名称或相对于classpath的路径
     * @return 数据集文件的全路径
     */
    private String getRealFileName(String fileName) {
        if (fileName.lastIndexOf('/') >= 0) {
            return fileName;
        } else {
            return this.packagePath + fileName;
        }
    }
}
